{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "# Collecting user input\n",
    "\n",
    "One frequently wants to collect interactively collect input from the user, for example to draw a curve or region of interest on a graph or to drill down into complex data sets.\n",
    "\n",
    "Matplotlib has support for collecting mouse events (motion, clicks, and scrolling) and keyboard events (key up/down + command keys) from the UI.  This machinery is how the pan / zoom / hotkeys configured by default in Matplotlib work, this tutorial will show you how to use this machinery for your own purposes.\n",
    "\n",
    "To start with, we will need to be using either the nbagg or ipypmpl and ensure that any print statements in our callbacks will make it back to the notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "# pick widget or notebook.  Widget works in Jupyter Lab, but requires ipympl to be installed\n",
    "%matplotlib notebook\n",
    "# nbagg ships as part of Matplotlib, but does not work in Jupyter Lab\n",
    "# %matplotlib widget\n",
    "# make sure prints come to the notebook\n",
    "%run helpers/ensure_print.py"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "To start lets interactively build a curve based on the points the users clicks on. To get notified when the user clicks we register a callback with the canvas.  This is the same idea as e.g. `on_click` in jquery or signal/slots in Qt, we provide a function that will be called on our behalf when the user takes some action.  To register a callabck we use the `canvas.mpl_connect` method which has the signature:\n",
    "\n",
    "```python\n",
    "def mpl_connect(self, s: str, func : Callable[Event]):\n",
    "    ...\n",
    "\n",
    "```\n",
    "\n",
    "`s` can be one of the 14 strings defining when we want our function called and the exact sub-class of `Event` that is passed in depends on the value of `s`.\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "\n",
    "## Button Press Callbacks\n",
    "\n",
    "\n",
    "[`MouseEvent`](https://matplotlib.org/3.1.1/api/backend_bases_api.html#matplotlib.backend_bases.MouseEvent) provides use acceses to what axes we are in, the mouse button that was clicked, any modified keys the user was holding down, and the location of the mouse.\n",
    "The attributes on the `event` that we are going to primarily conecerned with are:\n",
    "\n",
    "```python\n",
    "def callback(event : MouseEvent) -> None:\n",
    "    button = event.button   # the button clicked as an Enum\n",
    "    x = event.xdata         # the x location of the click in *data space*\n",
    "    y = event.ydata         # the y locatino of the click in *data space*\n",
    "    key = event.key         # any keyboard key that is held down while clicking\n",
    "```\n",
    "\n",
    "The values of  [MouseButton](https://matplotlib.org/3.1.1/api/backend_bases_api.html#matplotlib.backend_bases.MouseButton) we are interested in are\n",
    "\n",
    "```python\n",
    "from matplotlib.backend_bases import MouseButton\n",
    "MouseButton.LEFT == 1\n",
    "MouseButton.RIGHT == 3\n",
    "```\n",
    "\n",
    "In the browser both the middle and right button report as `MouseButton.RIGHT` because the right mouse button in the browser always opens a context menu. \n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "\n",
    "## Capture the Clicks\n",
    "\n",
    "As we are collecting state lets define a helper-class that is going to own the interaction and wrap around the `Line2D` artist that is drawing our curve.  To start with we will just print out _where_ the user clicked and with which button."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "from itertools import cycle\n",
    "# available on mpl >= 3.1\n",
    "# from matplotlib.backend_bases import MouseButton\n",
    "\n",
    "class LineMaker:\n",
    "    def __init__(self, ln):\n",
    "        # Stash the Line2D object, we will use this later\n",
    "        self.ln = ln\n",
    "        # register our method to be called when the mouse button is pressed down\n",
    "        self.button_cid = ln.figure.canvas.mpl_connect('button_press_event',\n",
    "                                                       self.on_button)\n",
    "\n",
    "    def on_button(self, event):\n",
    "        # print out what button and where the user clicked\n",
    "        print(f'button: {event.button!r} @ ({event.xdata}, {event.ydata}) + key: {event.key}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots()\n",
    "ln, = ax.plot([1], [1], '-o')\n",
    "line_maker = LineMaker(ln)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "source": [
    "### Exercise\n",
    "\n",
    "Only print when the left buton is pressed."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "skip"
    }
   },
   "source": [
    "Now we need to record where the user clicked and update the `Line2D` artist.  It is critical that we remember to call `draw_idle` at the end or the UI will not update to reflect our changes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "class LineMaker:\n",
    "    def __init__(self, ln):\n",
    "        # stash the Line2D artist\n",
    "        self.ln = ln\n",
    "        ln.axes.annotate('Left-click to add points', (.5, .9), \n",
    "                      ha='center', xycoords='axes fraction')\n",
    "        # register our method to be called per-click\n",
    "        self.button_cid = ln.figure.canvas.mpl_connect('button_press_event',\n",
    "                                                       self.on_button)\n",
    "\n",
    "    def on_button(self, event):\n",
    "        print(f'button: {event.button!r} @ ({event.xdata}, {event.ydata}) + key: {event.key}')\n",
    " \n",
    "        # only consider events from the lines Axes or if not the left mouse button bail! \n",
    "        if event.inaxes is not self.ln.axes or event.button != 1:\n",
    "            return\n",
    "   \n",
    "        # append the new point to the current Line2D data\n",
    "        xdata = list(self.ln.get_xdata()) + [event.xdata]\n",
    "        ydata = list(self.ln.get_ydata()) + [event.ydata]\n",
    "\n",
    "        # and update the data on the Line2D artist\n",
    "        self.ln.set_data(xdata, ydata)\n",
    "\n",
    "        # ask the UI to re-draw the next time it can\n",
    "        self.ln.figure.canvas.draw_idle()\n",
    "        \n",
    "    @property\n",
    "    def curve(self):\n",
    "        # get the current (x, y) for the line\n",
    "        return {'x': self.ln.get_xdata(), 'y': self.ln.get_ydata()}\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots()\n",
    "ln, = ax.plot([1], [1], '-o')\n",
    "line_maker = LineMaker(ln)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "skip"
    }
   },
   "source": [
    "We can use the the `curve` attribute on `line_maker` to get the current (x, y) data and when we are happy with it feed it into the next step of our analysis."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "skip"
    }
   },
   "outputs": [],
   "source": [
    "print(line_maker.curve)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "slideshow": {
     "slide_type": "skip"
    }
   },
   "source": [
    "### Exercise\n",
    "\n",
    "Being able to add points to our line is great!  However, what if we make a mistake and want to remove a point?  One way for the user to express they would like to remove the point is to hold down 'shift' while clicking.\n",
    "\n",
    "In this exercise, implement removing the nearest point from where the user clicked when they shift-click."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "class LineMakerRemover(LineMaker):\n",
    "\n",
    "    def on_button(self, event):\n",
    "        # print out what button and where the user clicked\n",
    "        print(f'button: {event.button!r} @ ({event.xdata}, {event.ydata}) + key: {event.key}')\n",
    " \n",
    "        # only consider events from the lines Axes or if not the left mouse button bail!\n",
    "        if event.inaxes is not self.ln.axes or event.button != MouseButton.LEFT:\n",
    "            return\n",
    "        \n",
    "        xdata = list(self.ln.get_xdata())\n",
    "        ydata = list(self.ln.get_ydata())\n",
    "\n",
    "        if event.key == 'shift':\n",
    "            print('in shift')\n",
    "            # TODO compute the closest (x, y) point and remove it from \n",
    "            # xdata, ydata\n",
    "        else:\n",
    "            # append the new point to the current Line2D data\n",
    "            xdata += [event.xdata]\n",
    "            ydata += [event.ydata]\n",
    "\n",
    "        # and update the data on the Line2D artist\n",
    "        self.ln.set_data(xdata, ydata)\n",
    "\n",
    "        # ask the UI to re-draw the next time it can\n",
    "        self.ln.figure.canvas.draw_idle()\n",
    "\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "slideshow": {
     "slide_type": "slide"
    }
   },
   "outputs": [],
   "source": [
    "fig = plt.figure()\n",
    "ax = fig.subplots()\n",
    "ln, = ax.plot([1], [1], '-o')\n",
    "line_maker = LineMaker(ln)"
   ]
  }
 ],
 "metadata": {
  "celltoolbar": "Slideshow",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
